#!/usr/bin/env python
# -*- coding: UTF-8 -*-
#
# Photo viewer based on the database that we created with addimages.py!
# Let's see how this one goes.
#
# VERSIONING HISTORY
#
# 20140209 v0.1 - First usable version, displays a single image by
#                 searching its id number in the images.db database.
#
# 20140209 v0.2 - Limited the image size to 700x700, able to see the
#                 entire picture this time!
#
# 20140220 v0.3 - Queries by location and is able to provide 
#                 thumbnail images of such list of images!!! OMG.
#
# 20140222 v0.4 - Updated a few layout issues, still not able to work
#                 around the non-scrollable thumbnails...
#                 Correctly added listing and clicking functionalities
#                 of thumbnails! Woo Hoo!
#
# 20140223 v0.5 - Able to search by person tagged and place likewise.
#                 Updated the viewing capabilities, maximizes the pic.
#
# 20140228 v0.6-rc - No longer required to carry the damn gif around.
#                    You can now switch databases on-the-fly if they
#                    have the correct column structure!
#
# 20140301 v0.6-rc2 - Date searching has been implemented, thus fulfilling
#                     all searching requirements envisioned.
#
# 20140304 v1.0 - Closed first sprint with requirements satisfied. For
#                 second, added capacity to search pictures by event.
#
# 20140312 v1.1 - Able to handle not having any database around to start.
#                 This will enable the user to choose from his own 
#                 available db files. Also, adds .jpg to the files!
#
# 20140817 v1.2 - Able to scroll up and down, thus viewing every picture that
#                 is returned by the search functions.
#
# TODO:
# - Add support for cross searching; that is, by People and Dates,
#   by People and Places, Places and Dates, etc.
# 
# - Allow all search fields to be present in the interface at all
#   times, that's how we're gonna implement searching!
# 
# - Implement a way of updating and deleting photo data.
#
# - Implement a way of showing picture metadata (date taken, place, etc)
#
# - Proposal: merge addimages.py with this one, thus making one single exec.
#

import os
import sys
import sqlite3 as sql
import Tkinter as tk
import tkFileDialog
import tkMessageBox
from PIL import Image, ImageTk
from cStringIO import StringIO


# Global variables for the database connection:
conn = sql.connect(":memory:")
cursor = conn.cursor()
people = ['None']
places = ['None']
events = ['None']
years = ['None']
months = ['01','02','03','04','05','06','07','08','09','10','11','12']

# This tiny controller will forbid the application from 
# shifting roots:
started = False

def update_database(db='images.db'):
    '''
    Create a list of distinct entries for locations and for people
    from the rows in the database. It looks for a database named 
    'images.db', unless another one is specified

    Because of the nature of the list creation, this method also 
    works like a validator, determining if the database can be used
    by the system or not.

    We need to run this once on startup, once we change databases
    and once everytime we "update" the database from elsewhere.
    '''
    global people
    global places
    global years
    global conn
    global cursor
    global events
    cursor.close()
    conn.close()
    conn = sql.connect(db)
    cursor = conn.cursor()

    # Test the database to see if it's indeed compatible:
    try:
        cursor.execute("SELECT location, people FROM photos WHERE photo_id = 1")
    except sql.OperationalError:
        print started
        if started:
            tkMessageBox.showerror(
                title="Arquivo inválido",
                message="""\
O arquivo escolhido não é compatível. 
Por favor, tente um outro arquivo, ou crie um novo."""
            )
        return
    people = []
    places = []
    years = []
    events = []

    cursor.execute("SELECT DISTINCT location FROM photos")
    places_raw = cursor.fetchall()

    for i in places_raw:
        places.append(i[0])

    conn.commit()

    places.sort()

    cursor.execute("SELECT DISTINCT people FROM photos")

    people_raw = cursor.fetchall()
    conn.commit()

    for row in people_raw:
        fullnames = row[0].split(", ")
        for name in fullnames:
            if name in people:
                pass
            else:
                people.append(name.lstrip())

    people.sort()

    cursor.execute("SELECT DISTINCT SUBSTR(date_taken, 1, 4) FROM photos")
    years_raw = cursor.fetchall()
    conn.commit()

    for row in years_raw:
        years.append(row[0])

    years.sort()

    cursor.execute("SELECT DISTINCT event FROM photos WHERE event IS NOT NULL")
    events_raw = cursor.fetchall()
    conn.commit()

    for row in events_raw:
        events.append(row[0])

    events.sort()

    del people_raw, places_raw, years_raw, events_raw

def listbyid(picid):
    '''
    Returns a single image buffer to be displayed in the
    GUI by PIL. Quintessential for displaying in spotlight!
    '''
    query = "SELECT photo_data FROM photos WHERE photo_id = ?"
    cursor.execute(query, (picid,))
    raw_images = cursor.fetchall()
    conn.commit()
    return raw_images[0]

def listbypeople(people):
    '''
    Search the database by people tagged. Returns a list of
    raw image data to be displayed in the GUi by PIL.
    '''
    if people == "":
        return []
    query = """
        SELECT photo_data, photo_id 
            FROM photos 
            WHERE people LIKE ?
            ORDER BY date_taken
        """
    cursor.execute(query, ('%'+people+'%',))
    raw_images = cursor.fetchall()
    conn.commit()
    raw_image_list = []
    for i in raw_images:
        raw_image_list.append(raw_images)
    print len(raw_image_list)
    return raw_image_list[0]
    

def listbylocation(location):
    '''
    Search the database by location. Returns a list of raw
    image data to be displayed in the GUI by PIL.

    Update: Query will return the ID as well to integrate
            with the id search function!
    '''
    if location == "":
        return []
    query = """
        SELECT photo_data, photo_id 
            FROM photos 
            WHERE location LIKE ?
            ORDER BY date_taken
        """
    cursor.execute(query, ('%'+location+'%',))
    raw_images = cursor.fetchall()
    conn.commit()
    raw_image_list = []
    for i in raw_images:
        raw_image_list.append(raw_images)
    print len(raw_image_list)
    return raw_image_list[0]

def listbyevent(event):
    '''
    Search the database by events that took place when the
    picture was taken. Returns a list of raw image data
    to be displayed by PIL
    '''
    if event == "":
        return []
    query = """
        SELECT photo_data, photo_id 
            FROM photos 
            WHERE event LIKE ? AND EVENT IS NOT NULL
            ORDER BY date_taken
        """
    cursor.execute(query, ('%'+event+'%',))
    raw_images = cursor.fetchall()
    conn.commit()
    raw_image_list = []
    for i in raw_images:
        raw_image_list.append(raw_images)
    print len(raw_image_list)
    return raw_image_list[0]

def listbydate(date_begin, date_end):
    '''
    Search the database for data in range between two dates.
    Return a list of image data to be displayed in the GUI by
    PIL pretty much in the same fashion as the previous methods,
    '''
    query = """
        SELECT photo_data, photo_id FROM photos 
            WHERE date_taken BETWEEN ? AND ? 
            ORDER BY date_taken
        """
    cursor.execute(query, (date_begin, date_end))
    raw_images = cursor.fetchall()
    conn.commit()
    raw_image_list = []
    for i in raw_images:
        raw_image_list.append(raw_images)
    print len(raw_image_list)
    return raw_image_list[0]


#--- GUI for displaying images and such ---
class UserInterface():
    '''
    This is the GUI of the program, where all the interaction with the
    user will happen. The functionality will be implemented and linked
    to through a simple form where the user will choose a search method
    (ex: search by people tagged, place taken, a range of dates in 
    which the picture was taken).

    The interface must present then one image in focus (a large view)
    and display every other picture found as thumbnails. Clicking the
    thumbnail causes the image in focus to be switched.

    Lastly, there must be a functionality that allows saving the images
    from the database to the filesystem.
    '''
    def show_image(self,event, code=None):
        self.token = code
        if code == None:
           self.token = self.queryfield.get()
        print "Searching for image ", self.token
        self.results = listbyid(self.token)
        self.buff_image = Image.open(StringIO(self.results[0]))

        # Crop the image if it's bigger than 700x700:
        self.photoconstraint1 = 700.0 / self.buff_image.size[0]
        self.photoconstraint2 = 700.0 / self.buff_image.size[1]
        
        # The smallest constraint is the one that must be used
        if self.photoconstraint1 > self.photoconstraint2:
            self.photolimit = self.photoconstraint2
        else:
            self.photolimit = self.photoconstraint1
        self.buff_image = self.buff_image.resize((int(self.photolimit * self.buff_image.size[0]), int(self.photolimit * self.buff_image.size[1])), Image.ANTIALIAS)
        
        self.buff_image = ImageTk.PhotoImage(self.buff_image)
        self.viewer.config(image=self.buff_image)

    def search_images(self, kind):
        '''
        Displays the thumbnails of all the images returned by some 
        listby*() method. The kind of search to be performed is given 
        by the [kind] argument
        
        If a new search is performed, this will have to clear the 
        thumbnails and then display the images attained from the new 
        query.
        '''
        self.search_token = self.drop_var.get()
        if kind == "location":
            self.raw_images = listbylocation(self.search_token)
        elif kind == "people":
            self.raw_images = listbypeople(self.search_token)
        elif kind == "event":
            self.raw_images = listbyevent(self.search_token)
        elif kind == "date":
            self.search_token = self.drop_var.get() + '-' + self.month_var.get()
            self.end_search_token = self.endyear.get() + '-' + self.endmonth.get()
            self.raw_images = listbydate(self.search_token, self.end_search_token)

        # I'm having trouble with deleting the existing overlay. This results in
        # the new images being added on to the current, definitely not good.
        # Maybe I should gc the whole thing every time?

        # SOLVED:
        # 1. Create a list '[]' to store the widgets created and destroyed on-the-fly.
        # 2. Upon demand, pack() and .append() them to the list "anonymously"
        # 3. To destroy them, iterate through the list and destroy them manually.
        #
        # Avoid using Exceptions here, they seem to make debugging harder.

        if len(self.thumblist) != 0:
            for thumb in self.thumblist:
                thumb[0].destroy()
            self.thumblist, self.imagethumblist = [], []

        # Hey, this is important: if you don't take the canvas back to its 
        # origin, it's not going to go back by itself. This is problematic
        # for cases in which 20+ images are found, then deleted.
        #self.resize_scrollables(None)
        for i in self.raw_images:
            self.image_id = i[1] 
            self.buff_thumb = Image.open(StringIO(i[0])) 

            # Crop the image since it's bigger than desired:
            self.photoconstraint1 = 150.0 / self.buff_thumb.size[0]
            self.photoconstraint2 = 150.0 / self.buff_thumb.size[1]
            
            # The smallest constraint is the one that must be used!
            if self.photoconstraint1 > self.photoconstraint2:
                self.photolimit = self.photoconstraint2
            else:
                self.photolimit = self.photoconstraint1
            self.imagethumb = self.buff_thumb.resize((int(self.photolimit * self.buff_thumb.size[0]), int(self.photolimit * self.buff_thumb.size[1])), Image.ANTIALIAS)
            
            self.imagethumb = ImageTk.PhotoImage(self.imagethumb)
            self.thumbnail = tk.Label(self.thumbcontainer, image=self.imagethumb)
            # DUDE! The mere act of pushing things in the list solved it!
            self.thumbimagelist.append(self.imagethumb)

            # These thumbnails aren't scrollable yet, which kind of
            # ruins the whole thing...
            self.thumblist.append((self.thumbnail, self.image_id))

        for thumb in self.thumblist:
            thumb[0].bind("<Button-1>", lambda event, arg=thumb[1]: self.show_image(event, code = arg) )
            thumb[0].pack()

    
    def save_image(self):
        '''
        Saves the current image in the preview as a file in the user's
        filesystem.

        I won't try to make this a bulk downloading method - one single
        image at a time, brah.
        '''
        self.candidate = listbyid(self.token)[0]
        with tkFileDialog.asksaveasfile(
                parent=self.window, mode="wb", 
                filetypes = [
                    ("Imagem JPEG", '*.jpg')
                ],
                title="Salvar imagem como...",
                defaultextension=".jpg"
            ) as self.output:
            self.output.write(self.candidate)

    def unhighlight(self, event):
        '''
        Paints the background of buttons back to the original color.
        '''
        self.saverbutton['bg'] = '#FA0'

    def switch_searches(self, towhere):
        '''
        Changes the query form between "Places," "People" and "Date."
        Available as a menu entry
        '''
        print "You have switched searches here."
        self.menulist.destroy()
        self.menu_fetcher.destroy()
        try:
            self.date_frame.destroy()
            self.date_frame2.destroy()
        except AttributeError:
            pass
        if towhere == "people":
            self.menulist = apply(tk.OptionMenu, (self.dropdownframe,self.drop_var) + tuple(people))
            self.menu_fetcher = tk.Button(self.dropdownframe, text="Pesquisar pessoa",
                command=lambda arg="people": self.search_images(kind=arg))
            self.menulist.pack(side='left')
        elif towhere == "place":
            self.menulist = apply(tk.OptionMenu, (self.dropdownframe,self.drop_var) + tuple(places))
            self.menu_fetcher = tk.Button(self.dropdownframe, text="Pesquisar locais",
                command=lambda arg="location": self.search_images(kind=arg))
            self.menulist.pack(side='left')
        elif towhere == "event":
            self.menulist = apply(tk.OptionMenu, (self.dropdownframe,self.drop_var) + tuple(events))
            self.menu_fetcher = tk.Button(self.dropdownframe, text="Pesquisar eventos",
                command=lambda arg="event": self.search_images(kind=arg))
            self.menulist.pack(side='left')
        elif towhere == "date":
            # Date searching is slightly different than the rest.
            self.date_frame = tk.Label(self.dropdownframe)
            self.date1_label = tk.Label(self.date_frame, text="De...")
            self.year_field = apply(tk.OptionMenu, (self.date_frame,self.drop_var) + tuple(years))
            self.month_field = apply(tk.OptionMenu, (self.date_frame,self.month_var) + tuple(months))
            
            self.date_frame2 = tk.Label(self.dropdownframe)
            self.date2_label = tk.Label(self.date_frame2, text="Para...")
            self.year_field2 = apply(tk.OptionMenu, (self.date_frame2,self.endyear) + tuple(years))
            self.month_field2 = apply(tk.OptionMenu, (self.date_frame2,self.endmonth) + tuple(months))

            self.menu_fetcher = tk.Button(self.dropdownframe, text="Pesquisar em datas",
                command=lambda arg="date": self.search_images(kind=arg))

            self.date_frame.pack()
            self.date1_label.pack(side='top')
            self.year_field.pack(side='left')
            self.month_field.pack(side='left')
            self.date_frame2.pack()
            self.date2_label.pack(side='top')
            self.year_field2.pack(side='left')
            self.month_field2.pack(side='left')
        else:
            raise ValueError, "invalid search option! I mean, how the fuck did you achieve this?"
        self.menu_fetcher.pack(side='left')

    def resize_scrollables(self, event):
        "This is required to reset the canvas"
        self.thumbnailframe.config(
                scrollregion=self.thumbcontainer.bbox("all"),
                width=200,
                height=700
        )

    def change_database(self):
        try:
            with tkFileDialog.askopenfile(parent=self.window, 
                title="Escolha um banco de dados",
                mode='rb',
                filetypes = [
                    ("Arquivos de dados","*.db")
                ]) as db:
                print "Using:", db.name
                update_database(db.name)
        except AttributeError:
            pass

    def __init__(self):
        # Initialize a few things that are needed to map the
        # thumbnails around dynamically:
        self.thumblist = []
        self.buff_thumb = None
        self.thumbimagelist = []
        
        # Widgets:
        self.window = tk.Tk()
        self.window.title("Visualizador de imagens")
        self.leftpane = tk.Frame(self.window, height=700, width=300)
        self.leftpane.pack_propagate(0)
        self.idqueryframe = tk.Frame(self.leftpane)
        self.rightpane = tk.Frame(self.window, width=700, height=700)
        self.rightpane.pack_propagate(0)
        self.dropdownframe = tk.Frame(self.leftpane)
        self.scrollableframe = tk.Frame(self.leftpane, height=700)
        self.scrollableframe['bg'] = "#BBB"
        self.thumbnailframe = tk.Canvas(self.scrollableframe)
        self.thumbcontainer = tk.Frame(self.thumbnailframe)
        self.scrolly = tk.Scrollbar(self.thumbnailframe, command=self.thumbnailframe.yview)
        self.saverbutton = tk.Button(self.rightpane, text="Salvar Imagem",
                                     command=self.save_image, bg = "#FA0")

        self.saverbutton.bind("<Leave>", self.unhighlight)
        self.saverbutton['cursor'] = "hand1"

        # Going WIMP by adding a menu:
        self.menubar = tk.Menu(self.window)
        self.filemenu = tk.Menu(self.menubar, tearoff=0)
        self.filemenu.add_command(label="Salvar imagem", command=self.save_image)
        self.filemenu.add_command(label="Sair", command=self.window.quit)
        self.menubar.add_cascade(label="Arquivo", menu=self.filemenu)
        self.searchmenu = tk.Menu(self.menubar, tearoff=0)
        self.searchmenu.add_command(label="Por pessoas", command=lambda a="people": self.switch_searches(towhere=a))
        self.searchmenu.add_command(label="Por lugar", command=lambda a="place": self.switch_searches(towhere=a))
        self.searchmenu.add_command(label="Por data", command=lambda a="date": self.switch_searches(towhere=a))
        self.searchmenu.add_command(label="Por evento", command=lambda a="event": self.switch_searches(towhere=a))
        self.menubar.add_cascade(label="Pesquisar...", menu=self.searchmenu)
        self.configmenu = tk.Menu(self.menubar, tearoff=0)
        self.configmenu.add_command(label="Escolher banco de dados", command=self.change_database)
        self.menubar.add_cascade(label="Configurar", menu=self.configmenu)

        # Make it scrollable:
        self.thumbnailframe.config(yscrollcommand = self.scrolly.set)

        #Stuff for dropdown
        self.drop_var = tk.StringVar(self.window) # People, places, one year
        self.month_var = tk.StringVar(self.window) # one month
        self.endyear = tk.StringVar(self.window) # the other year
        self.endmonth = tk.StringVar(self.window) # the other month
        self.drop_var.set("Escolha um local...")
        self.menulist = apply(tk.OptionMenu, (self.dropdownframe,self.drop_var) + tuple(places))
        self.menu_fetcher = tk.Button(self.dropdownframe, text="Pesquisar",
                command=lambda arg="location": self.search_images(kind=arg))

        self.viewer = tk.Label(self.rightpane)

        #Pack the widgets!
        self.leftpane.pack(side='left')
        self.rightpane.pack(side='right')
        self.dropdownframe.pack()
        self.scrollableframe.pack(fill=tk.BOTH, expand=1)
        self.thumbnailframe.pack(fill=tk.BOTH, expand=1)
        self.thumbcontainer.pack(fill=tk.BOTH, expand=1)
        self.thumbnailframe.create_window((0,0),window=self.thumbcontainer, anchor='nw')

        self.idqueryframe.pack()
        self.menulist.pack(side='left')
        self.menu_fetcher.pack(side='left')
        self.saverbutton.pack(side='bottom')
        self.scrolly.pack(side='right', fill='y')
        self.viewer.pack(expand=1)
        self.window.config(menu=self.menubar)

    def main(self):
        self.window.mainloop()

if __name__ == "__main__":
    # We can't simply create a new database if we don't have one.
    # EDIT: Hey, we can't create, but update_db() allows us to 
    #       change bases easily. Not the end of the world.
    if 'images.db' not in os.listdir('.'):
        update_database(db=":memory:")
        print "ERROR: database not found!"
        app = UserInterface()
        tkMessageBox.showerror(
            message="""\
Could not find a database to work on!

Please check to see if 'images.db' exists.
            """,
            title="Error"
        )
    else:
        update_database()
        app = UserInterface()
    started = True
    app.main()
    cursor.close()
    conn.close()
